home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Gekkan Dennou Club 143
/
Gekkan Dennou Club - 2000.4 Vol. 143 (Japan).7z
/
Gekkan Dennou Club - 2000.4 Vol. 143 (Japan).bin
/
docs
/
perl
/
perl17.doc
< prev
next >
Wrap
Text File
|
2000-02-26
|
17KB
|
602 lines
おきらくPerlプログラミング入門
~~めざせ Perl マスター~~
広井 誠
第17回
○オブジェクト指向編(その3)
今回はオブジェクト指向の目玉機能である「継承」を説明します。Perl と他
のオブジェクト指向言語を比べると、継承の基本的な考え方は同じですが、そこ
は Perl のことですから、やっぱり個性的なのです。C++をご存じの方でも、きっ
と驚かれることでしょう。
○継承
前々回説明したように、継承はクラスに「親子関係」を持たせる機能です。子
供のクラスは親クラスの性質を受け継ぐことができます。あるクラスを継承する
場合、その元になるクラスを「スーパークラス」と呼びます。そして、継承した
クラスを「サブクラス」と呼びます。この呼び方はプログラミング言語によって
まちまちで統一されていません。C++の場合は、元になるクラスを「基本クラス」
といい、継承するクラスを「派生クラス」とか「導出クラス」といいます。
継承には「単一継承」と「多重継承」の二種類があります。単一継承は、ただ
一つのクラスからしか機能を継承することができません。したがって、クラスの
階層は次のような木構造で表すことができます。
A
/|\
/ | \
B C D
/ \
/ \
E F
図 1 : 単一継承におけるクラスの階層
継承は何段階に渡って行われてもかまいません。たとえばクラスEの場合、スー
パークラスがBで、BのスーパークラスがAに設定されています。サブクラスは
複数あってもかまいません。たとえば、AのサブクラスはB、C、Dと3つ、B
のサブクラスはE、Fと2つあります。上図では、クラスAのスーパークラスは
ありませんが、他のクラスではただ一つのスーパークラスを持っています。プロ
グラミング言語では、Java や Smalltalk、拙作の Lisp インタプリタ VTOL が
単一継承です。
これに対し多重継承は、複数のクラスから機能を継承することができます。こ
のため、クラスの階層は木構造ではなく、次のようなグラフで表すことができま
す。
A
/ \
/ \
B C
/ \ / \
/ \ / \
D E F
図 2 : 多重継承におけるクラスの階層
クラスEに注目してください。スーパークラスにはBとCの2つがあります。多
重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつ、スー
パークラスも複数持つことができるのです。C++や Common Lisp Object System
(CLOS)は多重継承をサポートしています。そして肝心の Perl は何と多重継承な
のです。
実を言うと、筆者は多重継承に対してあまりいいイメージを持っていません。
そのため、拙作の VTOL は単一継承となったのです。私見ですが、多重継承はメ
リットよりもプログラムを複雑にするデメリットの方が大きいのではないか、と
思っています。特に、図 2 のクラスA、B、C、Eのような菱形の関係をC++
でプログラムする場合、とても複雑な問題を引き起こすことが知られています。
ところが Perl の場合、継承であれば当然持っているはずの機能が削除されてい
て、そのことにより多重継承の複雑さはC++よりも軽減されているように思いま
す。そこで、まず一般的な継承の仕組みから説明しましょう。
一般のオブジェクト指向言語の場合、継承によって引き継がれる性質は定義さ
れたデータやメソッドとなります。データを受け継ぐことを「属性の継承」、メ
ソッドを受け継ぐことを「実装の継承」と区別して呼ぶことがあります。次の図
を見てください。
class
┌─ Foo ─┐ ┌─ instance ─┐
├─────┤ ├───────┤
│ 変数 a │────→│ 変数 a │
├─────┤ ├───────┤
│ 変数 b │ │ 変数 b │
└─────┘ └───────┘
method : get_a(), get_b()
│
継承
↓
┌─ Bar ─┐ ┌─ instance ─┐
├─────┤────→├───────┤
│ 変数 c │ │ 変数 a │
└─────┘ ├───────┤
method : get_c() │ 変数 b │
├───────┤
│ 変数 c │
└───────┘
図 3 : 一般のオブジェクト指向言語における継承
クラス Foo には変数 a, b [*1] とアクセスメソッド get_a(), get_b() が定
義されています。次に、クラス Bar を定義します。Bar は Foo を継承し、Bar
固有の変数 c とアクセスメソッド get_c() が定義されています。Foo と Barの
インスタンスを生成すると、図に示したように、Bar のインスタンスにはクラス
Foo で定義された変数 a, b も含まれます。これが属性の継承です。Foo のイン
スタンスを生成すると、もちろん変数 a, b は含まれていますが、Bar のインス
タンスとメモリを共有することはありません。クラスはオブジェクトの設計図で
す。設計に共通な部分があったとしても、それから生み出されるインスタンスは
別々の実体で、メモリを共有することはないのです。
クラス Bar にはメソッド get_c() しか定義されていませんが、クラス Fooを
継承したことにより、メソッド get_a() と get_b() を利用することができます。
これが実装の継承です。bar のインスタンスに対して get_a() を呼び出すと、
クラス bar には get_a() が定義されていないので、スーパークラス Foo を調
べ、そこで定義されている get_a() が呼び出されます。もちろん、取り出され
る値は、Bar のインスタンスにある変数 a の値です。
一般のオブジェクト指向言語では、このように属性と実装が継承されるのです
が、Perl はそうではありません。実装は継承されますが、属性は継承されませ
ん。つまり、Perl では継承されるのはメソッドだけなのです。いやはやなんと
も、Perl のオブジェクト指向は筆者が常識だと思っていたことを次々と破壊し
てくれます。このようなオブジェクト指向もあるのだなあ、とたいへん驚きまし
た。このように Perl では属性の継承が行われないため、スーパークラスとサブ
クラスで共通に使用するデータ構造は、プログラマが決定する必要があります。
といっても難しい話ではなく、ハッシュを使えばうまくいきます。
note:
[*1] インスタンス内に保持される変数を「インスタンス変数」と呼ぶことがあ
ります。
それでは具体的に Perl の継承を説明していきましょう。Perl の継承は、パッ
ケージに用意されている特別な配列 @ISA で定義します。@ISA にセットしたク
ラス名(パッケージ名)がスーパークラスとなります。この @ISA に複数のクラ
ス名を定義すると、それら複数のクラスを継承する多重継承となります。継承に
必要な設定はこれだけです。では実際に図 3 のクラスをプログラムしてみましょ
う。まずクラス Foo を定義します。
List 1 : クラス Foo の定義
1 package Foo;
2
3 # インスタンスの生成
4 sub new {
5 my ($type, $a, $b) = @_;
6 my $obj = { 'a' => $a, 'b' => $b };
7 bless $obj, $type;
8 $obj;
9 }
10
11 # アクセスメソッド
12 sub get_a {
13 my $obj = shift;
14 $obj->{'a'};
15 }
16
17 sub get_b {
18 my $obj = shift;
19 return $obj->{'b'};
20 }
インスタンスを生成するメソッドを「コンストラクタ」と呼びます。元々はC++
の用語でが、Perl でも使われることがあります。コンストラクタを継承できる
ように、bless には引数で与えられたクラス名 $type を渡します。前回は、ク
ラス名を省略するとメソッドの継承が動作しないと書きましたが、それだけでは
なく、クラス名を直接書いてもコンストラクタの継承は動作しないのです。たと
えば、Foo を継承するクラス Foo1 を考えましょう。Foo1 には新しいインスタ
ンス変数を定義せずに、メソッドだけ追加することにします。この場合、Foo の
コンストラクタをそのまま継承できるはずです。つまり、Foo1->new() は Foo
の new() を呼び出しますが、それでも Foo1 のインスタンスを生成しなくては
いけません。ところが、bless で Foo を直接指定すると、Foo1->new() と呼び
出しても作成するインスタンスは Foo のインスタンスになってしまいます。こ
のため、bless には直接クラス名を記述するのではなく、引数で与えられたクラ
ス名を渡すようにしてください。
メソッドの定義は簡単ですね。与えられたインスタンスから値を取り出すだけ
です。次にクラス Bar を定義します。
List 2 : クラス Bar の定義
1 package Bar;
2 @ISA = (Foo); # Foo を継承
3
4 # インスタンスの生成
5 sub new {
6 my ($type, $a, $b, $c) = @_;
7 my $obj = { 'a' => $a, 'b' => $b, 'c' => $c };
8 bless $obj, $type;
9 $obj;
10 }
11
12 # アクセスメソッド
13 sub get_c {
14 my $obj = shift;
15 return $obj->{'c'};
16 }
まず @ISA にスーパークラス Foo をセットします。クラス Bar は新しいイン
スタンス変数 $c を使うので、自分のコンストラクタ new() を定義します。こ
のように、継承したクラスのメソッドとは違う働きをさせる場合、同名のメソッ
ドを定義することで行うことができます。これを「オーバーライド(override)」
といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の
動作です。メソッドはサブクラスからスーパークラスに向かって検索しますので、
スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるのは当然
のことなのです。
メソッドをオーバーライドした場合、サブクラスのメソッドからスーパークラ
スのメソッドを呼び出すことができると便利です。List 2 の 7 行目で、無名の
ハッシュにデータをセットしていますが、スーパークラスのコンストラクタを呼
び出してインスタンスを生成し、そこにキー 'c' を追加することもできます。
この場合は、次のようなプログラムになります。
List 3 : インスタンスの生成(その2)
1 sub new {
2 my ($type, $a, $b, $c) = @_;
3 my $obj = $type->Foo::new( $a, $b );
4 $obj->{'c'} = $c;
5 $obj;
6 }
このように、オブジェクトにハッシュを使うことで、インスタンス変数の追加
を簡単に行うことができます。Perl の場合、オブジェクトに配列を使うことも
できますが、配列は末尾方向にしか拡張することができないため、どのサブクラ
スが何番目の要素を使うかで問題が発生します。ハッシュを使えば、同じ名前を
使わないように注意するだけですみます。
ところが、これではクラス名 Foo を直接書いているため、継承の階層を変更
した時、たとえば Foo と Bar の間に新しいクラスを挿入する場合、プログラム
を修正しなくてはなりません。これを避けるため、Perl には SUPER という疑似
クラスが用意されています。
List 4 : インスタンスの生成(その3)
1 sub new {
2 my ($type, $a, $b, $c) = @_;
3 my $obj = $type->SUPER::new( $a, $b );
4 $obj->{'c'} = $c;
5 $obj;
6 }
これで @ISA の中からクラスを検索し、適切なコンストラクタが呼び出されま
す。Foo と Bar の間に新しいクラスを挿入するにしても、配列 @ISA を変更す
るだけで済むのです。ただし、激光電脳倶楽部で配布された Perl version 5.001
では、残念ながら SUPER 疑似クラスは動作しないようです。Windows など他の
環境で Perl をお持ちの方は試してみてください。
それでは実行してみましょう。
$o1 = Foo->new( 1, 2 );
$o2 = Bar->new( 10, 20, 30 );
print $o1->get_a(), "\n"; # 1 を出力
print $o2->get_a(), "\n"; # 10 を出力
print $o2->get_c(), "\n"; # 30 を出力
メソッド get_a() は Bar に定義されていませんが、スーパークラス Foo のメ
ソッド get_a() が呼び出されて、変数 a の値を求めることができます。また、
Bar のインスタンスに対して get_c() を呼び出せば、変数 c の値を求めること
ができます。
○多重継承
次は、多重継承を説明します。簡単な例題として、Foo と Bar の2つのクラ
スを継承するクラス Baz を考えてみましょう。まず、Foo と Bar を定義します。
List 5 : クラス Foo の定義
1 package Foo;
2
3 sub new {
4 my ($type, $a) = @_;
5 my $obj = {'a' => $a };
6 bless $obj, $type;
7 $obj; }
8
9 sub get_a {
10 my $obj = shift;
11 $obj->{'a'}; }
12
13 sub method_1 {
14 print "Foo::method_1\n"; }
List 6 : クラス Bar の定義
1 package Bar;
2
3 sub new {
4 my ($type, $b) = @_;
5 my $obj = {'b' => $b };
6 bless $obj, $type;
7 $obj;
8 }
9
10 sub get_b {
11 my $obj = shift;
12 $obj->{'b'};
13 }
14
15 sub method_1 {
16 print "Bar::method_1\n";
17 }
クラス Foo にはインスタンス変数 $a とアクセスメソッド get_a()、クラス Bar
にはインスタンス変数 $b とアクセスメソッド get_b() が定義されています。
そして、両方のクラスともメソッド method_1() が定義されています。Foo と
Bar を継承するクラス Baz は、次のように定義されます。
List 7 : クラス Baz の定義
1 package Baz;
2 @ISA = (Foo, Bar);
3
4 sub new {
5 my ($type, $a, $b) = @_;
6 my $obj = {'a' => $a, 'b' => $b };
7 bless $obj, $type;
8 $obj;
9 }
配列 @ISA に Foo と Bar をセットします。これで Foo と Bar を継承すること
ができます。さっそく実行してみましょう。
$o1 = Baz->new(10,20);
print $o1->get_a(), "\n"; # 10 を表示
print $o1->get_b(), "\n"; # 20 を表示
$o1->method_1(); # Foo::method_1 と表示
継承したメソッド get_a(), get_b() を呼び出すことができるのは当然ですが、
両方のクラスにある method_1() は、どちらが呼び出されるのでしょう。表示さ
れた Foo::method_1 から、クラス Foo のメソッドが呼び出されたことがわかり
ます。このように、メソッドの検索は配列 @ISA の先頭から順番に行われ、最初
に見つかったメソッドが実行されます。したがって、クラスの順番を逆にすると、
今度は Bar::method_1 と表示されます。
では、Foo と Bar にスーパークラスが設定されている場合はどうなるのでしょ
うか。この場合、メソッドは「深さ優先」で探索されます。次の図を見てくださ
い。
A B C
│ │ │
│ │ │
D E F
\ │ /
\│/
G
G→D→A→E→B→F→C
図 4 : 多重継承におけるメソッドの探索
クラスGは、クラスD、E、Fを多重継承しています。D、E、Fのスーパーク
ラスはそれぞれA、B、Cです。クラスGで @ISA = (D, E, F) と設定されてい
るとすると、最初にクラスDのメソッドを探索します。次は深さ優先で探索する
ので、クラスEではなくクラスAを探索します。このように、スーパークラスを
優先して探索し、それでも見つからない時はクラスEを探索します。したがって、
探索順序は「G→D→A→E→B→F→C」となるのです。上図を経路と考えれ
ば、まさに深さ優先探索そのものですね。
ちなみにC++の場合、多重継承したクラスに同名のメソッドがある場合、どち
らを呼び出すのか明確に指定しないとコンパイルでエラーとなります。またC++
は属性の継承も行われるため、変数名の衝突も発生します。この場合も、どちら
の変数を使用するのか明確に指定しないとコンパイルエラーとなります。この他
にも、多重継承ではいろいろな問題が発生するため、それを解決するためにC++
ではいろいろな機能が用意されています。ところが、それらの機能がC++をいっ
そう複雑な言語にしていると、筆者には思えてなりません。まあ、C++はコンパ
イラ型の言語で、なによりも効率を重視するため、Perl のようなインタプリタ
型の言語よりも複雑な言語仕様になるのは避けられないのかもしれません。
○間接記法
ところで、Perl には多種多用な表記方法が用意されていますが、メソッドの
呼び出しにも矢印記法だけではなく「間接記法」という別の方法があります。こ
れは次のように、オブジェクト名やクラス名よりも先にメソッド名を書く方法で
す。
METHOD CLASS_OR_INSTANCE LIST
メソッド名とクラスまたはインスタンスの間にカンマ (,) を入れてはいけませ
ん。引数を複数渡す場合はカンマで区切ります。具体的には、次のようになりま
す。
$o1 = new Baz 10, 20;
引数を括弧で囲んでもかまいません。
$o1 = new Baz( 10, 20 );
こうすると、C++でよく出てくる表記法と同じになります。ただし、間接記法は
矢印記法より曖昧であることに注意してください。次の例を見てください。
method $obj->{'key'};
method はメソッドですが、インスタンスは $obj か $obj->{'key'} のどちらに
なるのでしょう。この場合、Perl は $obj をインスタンスと判断し、method $obj
を実行します。そして、その結果はハッシュのリファレンスでなければいけませ
ん。$obj->{'key'} のメソッドを呼び出す場合は、次のように矢印記法を使った
方がいいでしょう。
$obj->{'key'}->method();
こちらの方がはっきりとわかりますね。
○次回は?
今回は説明のためのプログラムしか示すことができなかったので、次回は継承
を使って具体的なプログラムに挑戦してみましょう。お楽しみに。
―参考文献―
[1] Larry Wall, Tom Christiansen, Randal L. Schwartz 共著「プログラミン
グPerl」改訂版 オライリー・ジャパン 1997
[2] Sriram Srinivasan 著「実用Perlプログラミング」オライリー・ジャ
パン 1998
(EOF)